#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#include <fcntl.h>

#include <glob.h>
#include <unistd.h>
#include <sys/wait.h>

#include "util.h"
#include "list.h"

#define BUFFSIZE 1024



char* string_copy(char* string)
{
    int len = strlen(string) + 1;
    char* copy = malloc(len*sizeof(char));
    strcpy(copy, string);
    return copy;
}

char* get_line()
{
    static char buffer[BUFFSIZE];
    char* retval = fgets(buffer, BUFFSIZE, stdin);
    if(retval == NULL)
    {
        return NULL;
    }
    chomp(buffer);
    return string_copy(buffer);   
    
}

typedef enum
{
    SIMPLE,  // simple command
    IN,      // '<' 
    OUT,     // '>'
    PIPE,    // '|'
    
    EMPTY,   // input empty
    EOT,     // end of input
    NOFILE,  //file could not be opened for reading
    EXIT,
    
} type_t;

typedef struct 
{
    type_t type;
    List* contents;
    bool is_bg;
    bool is_in;
    char* in_file;
    char* out_file;
    
} command_t;


char* add_space(char* input)
{
    CHECK_ARG(input);
    int len = strlen(input) + 2;
    char* copy = malloc(sizeof(char)*len);
    strcpy(copy, input);
    copy[len-1] = '\0';
    copy[len-2] = ' ';
    //free(input);
    return copy;
    
}

/*
  Expand the given pattern, returning a list of the matching terms.
  If this function fails, it returns a list containing the argument.
*/
List* globify(const char* pattern)
{
    glob_t* globber = malloc(sizeof(glob_t));
    CHECK_PTR(globber);
    int retval = glob(pattern, GLOB_NOCHECK, NULL, globber);
    if (retval != 0)
    {
        List* temp = List_create(1);
        List_push(temp, pattern);
        return temp;
    }
    List* result = List_create(globber->gl_pathc);
    unsigned int i;
    for(i = 0; i < globber->gl_pathc; i += 1)
    {
        if (globber->gl_pathv[i] == NULL)
        {
            die("glob returned NULL unexpectedly\n");
        }
        List_push(result, globber->gl_pathv[i]);
    }
    
    globfree(globber);
    return result;
    
} 

bool is_whitespace(char c)
{
    return c == ' ' || c == '\t';
}

void eat_whitespace(const char* line, int start, int* end)
{
    int max = strlen(line);
    int i = start;
    while(is_whitespace(line[i]))
    {
        i += 1;
    }
    if (i > max - 1) die("unexpected increment\n");
    *end = i;
    return;
}

/*
  When this function returns, `end` will point to the first whitespace 
  character after the returned word, or past the end of `line`.
*/
char* eat_word(const char* line, int start, int* end)
{
    static char buff[BUFFSIZE];
    int i;
    int k = 0;
    int max = strlen(line);
    if (start == max - 1 || is_whitespace(line[start])) 
    {
        die("Function eat_word violated precondition.\n");
    }
    for (i = start; /*nothing*/; (k += 1, i += 1))
    {
        if (i == max - 1 || is_whitespace(line[i]))
        {
            buff[k] = '\0'; //terminate the string
            *end = i;
	    
            return string_copy(buff);
        }
        
        buff[k] = line[i];
        
    }
    die("impossible\n");
    
}

List* split(char* line)
{
    static char buff[BUFFSIZE];
    List* result = List_create(100);
    int max = strlen(line);
    int i;
    for (i = 0; i < max - 1; i += 1)
    {
                
        eat_whitespace(line, i, &i);
        if (i >= max - 1)
        {
            break;
        }
        char* word = eat_word(line, i, &i);
        
        List_push(result, word);
        free(word);
    }
    return result;
}

bool contains(const char* string, char c)
{
    int len = strlen(string);
    int i;
    for (i = 0;i < len; i += 1)
    {
        if (string[i] == c) return true;
    }
    return false;
}

void free_command(command_t* command)
{
    if (command == NULL) return;
    
    
    free(command);

}

command_t* get_command()
{
    command_t* command = malloc(sizeof(command_t));
    command->in_file = NULL;
    command->out_file = NULL;
    command->contents = NULL;

    type_t type = -1;
    char* line = get_line();
    if (line == NULL) 
    {
        command->type = EOT;
        return command;
    }
    if (strlen(line) == 0) 
    {
        command->type = EMPTY;
        return command;
    }
    char* spaced = add_space(line);
    
    List* raw = split(spaced);
    List* expanded = List_create(100);
    int i;
    int len = raw->count;
    for (i = 0; i < len; i += 1)
    {
        const char* current = raw->array[i];
        if (contains(current, '*'))
        {
            List* globbed = globify(current);
            int k;
            for (k = 0; k < globbed->count; k += 1)
            {
                List_push(expanded, globbed->array[k]);
            }
            List_free(globbed);
        }
        else
        {
            List_push(expanded, current);
        }
    }
    int count = expanded->count;
    if (count >= 0)
    {
	if (count == 1 && (strcmp(expanded->array[0], "exit") == 0))
        {
	    type = EXIT;
        } 
	else // count > 1
        {
	    if (strcmp(expanded->array[count - 1], "&") == 0)
            {
		command->is_bg = true;
		List_pop(expanded);
		count -= 1;
            }
	    
	    count = expanded->count;
	    if (count >= 3 && (strcmp(expanded->array[count-2], ">") == 0))
            {
		
                command->out_file = List_pop(expanded);
		List_pop(expanded);
            }
            else
            {
                command->out_file = NULL;
            }

            count = expanded->count;

            if (count >= 3 && (strcmp(expanded->array[count-2], "<") == 0))
            {
		command->in_file = List_pop(expanded);
		List_pop(expanded);
            }
            else
            {
                command->in_file = NULL;
            }


	    
	    
            if (List_find(expanded, "|"))
            {
		die("piped command not implemented\n");
            }
	    else
            {
		type = SIMPLE;
		
            }
        }
    }
    else
    {
	type = EMPTY;
    }
    
    
    
    
    
    free(line);
    free(raw);
    
    List_push(expanded, NULL); // null terminator for execvp
    
    
    if (type == (type_t)(-1))
    {
        printf("Invalid command\n");
        return NULL;
    }
    command->type = type;
    command->contents = expanded;
    return command;
}

void print_command(command_t* command)
{
    List* list = command->contents;
    int i;
    for (i = 0; i < list->count; i += 1)
    {
        printf("%s ", list->array[i]);
    }
    printf("\n");
}



void execute(command_t* command)
{
   
    if (command->type == EXIT)
    {
        wait(0);
        fflush(stdout);
        exit(0);
    }    
	
    if (fork() != 0)
    {
        if (!command->is_bg)
        {
            int i = 3;
            for (; i < _SC_OPEN_MAX; i += 1)
            {
                close(i);
            }
            wait(0);
        }
        return;
    }

   
	
    if (command->in_file != NULL)
    {
        close(STDIN_FILENO);
        int retval = open(command->in_file, O_RDONLY);
        if (retval == -1)
        {
            printf("file could not be opened\n");
            return;
        }
        
    }
    if (command->out_file != NULL)
    {
        
        /* create the file if it doesnt exist */
        FILE* file = fopen(command->out_file, "w");
        if (file == NULL)
        {
            printf("Unable to open file '%s' for writing\n", command->out_file);
            return;
        }
        //fputs(" ", file);
        fclose(file);

        close(STDOUT_FILENO);
        int retval = open(command->out_file, O_WRONLY);
        if (retval == -1) 
        {
            die("Failed to redirec output to '%s'\n", command->out_file);
        }
	
    }
    char** args = command->contents->array;
    execvp(args[0], args);
    printf("Failed to execute program\n");
    print_command(command);
} 

int main()
{
    while (true)
    {
        fflush(stdin);
        printf("shell$ ");
	fflush(stdout);
        command_t* command = get_command();
        if (command->type == EMPTY)
        {
	    continue;
        }
	if(command->type == EOT)
        {
	    return 0;
        }
	if (command->type == NOFILE)
        {
	    printf("File '%s' could not be opened for reading.\n", command->in_file);
	    continue;
        }

        execute(command);

        free_command(command);
    }
    
    return 0;
}



